iT邦幫忙

第 12 屆 iThome 鐵人賽

DAY 8
0
Software Development

從生活中認識Design Pattern系列 第 8

[Day08] 合成/聚合複用原則 | Composite/Aggregate Reuse Principle

  • 分享至 

  • xImage
  •  

本文同步分享於個人blog

合成是什麼?聚合又是什麼?他們與繼承差在哪???

  • 定義


盡量使用組合(contains-a)/聚合(has-a)方式來代替繼承(is-a)來達到重複使用的目的

在軟體開發中,一定會遇到套件重複使用的問題。CARP主要目的在於當要重複使用套件時,應該先考慮使用組合/聚合的方式,其次才是繼承。而如果要使用繼承的話,則須符合里氏替換原則(LSP)

  • 合成 v.s 聚合


在比較合成/聚合與繼承之間的差異性時,首先要先了解合成是什麼?聚合又是什麼?

我們可以把這兩個行為理解成整體與部分對象的"擁有"關係。

聚合:班級與學生

在聚合中,部分對象及整體對象兩者的生命週期並沒有關聯,部分對象甚至可能超過整體對象。例如部分對象是學生,而整體對象是班級。一個班級畢業班級就不存在了,但學生依舊繼續存在於另一個整體對象,如大學或是某間公司。學生(部分對象)的生命週期並沒有因為班級(整體對象)結束而跟著結束。

class Clazz {
    public void request(Student student){
        System.out.println("class點名!!");
        student.response();
    }
}

class Student {
    public void response(){
        System.out.println("student喊在!!");
    }
}

public class MyClass {
    public static void main(String args[]) {
      Clazz c = new Clazz();
      Student s = new Student();
      c.request(s);
    }
}

output

class點名!!
student喊在!!
合成:人與器官

相較於聚合,合成的擁有關係就相對強烈一點。部分對象以及整體對象的生命週期是一致的。如人與他的心臟。如果他人死了,那心臟也跟著停止跳動。相對於聚合,合成擁有較強的關聯性。

class People {
    Heart h = new Heart();
    public void request(){
        System.out.println("people走路!!");
        h.response();
    }
}

class Heart {
    public void response(){
        System.out.println("heart跳一下!!");
    }
}

public class MyClass {
    public static void main(String args[]) {
      People c = new People();
      c.request();
    }
}

output

people走路!!
heart跳一下!!

經由上面兩個例子可以發現,Clazz及Student的關聯性低,就算Clazz結束了也跟Student無關。而相較於前一個例子,People與Heart的關係就比較緊密,若People生命週期結束了Heart也會跟著結束。

  • 合成/聚合 v.s. 繼承


比較完合成及聚合的差異,現在來看看合成/聚合跟繼承又差在哪呢?

1. 封裝性
  • 繼承時父類別的實作細節會暴露給子類別,所以繼承會破壞類別的封裝。而由於父類別對子類別是透明的,所以繼承又稱做白箱複用
  • 合成/聚合時使用的類別並不知道原始類別的實作細節,所以維持了類別的封裝性,又稱做黑箱複用
2. 耦合度
  • 繼承時父類別與子類別的耦合度高。若父類別做任何更動都會影響到子類別,降低了類別的擴充及維護。
  • 合成/聚合時新舊類別的耦合度及依賴程度低,新類別只能透過複用類別的接口取得內容。
3. 靈活性
  • 繼承限制了整體的靈活性。由於從父類別繼承下來的實作是靜態的,不可能在執行時發生變化。
  • 合成/聚合的靈活性較高。合成/聚合執行時,新類別可以自由的引用與組合類別相同的對象。
  • 實作Composite/Aggregate Reuse Principle


剛剛簡述了一下合成/聚合以及繼承的差別,可能還有些模糊,那我們來舉個例子。車子有分電動車以及汽油車,各有白色及黑色兩款,如下:

CARP1

abstract class Car {
     abstract void run();
}

class ElectricCar extends Car {
    @Override
    void run() {
        System.out.println("電動車");
    }
}
class PetrolCar extends Car {
    @Override
    void run() {
        System.out.println("汽油車");
    }
}
class BlackElectricCar extends ElectricCar {
    public void appearance() {
        System.out.print("黑色");
        super.run();
    }
}
class BlackPetrolCar  extends PetrolCar {
    public void appearance() {
        System.out.print("黑色");
        super.run();
    }
}
class WhiteElectricCar extends ElectricCar {
    public void appearance() {
        System.out.print("白色");
        super.run();
    }
}
class WhitePetrolCar extends PetrolCar {
    public void appearance() {
        System.out.print("白色");
        super.run();
    }
}
public class MyCar {
    public static void main(String args[]) {
        WhiteElectricCar whiteElectricCar = new WhiteElectricCar();
        whiteElectricCar.appearance();
    }
}

output

白色電動車

這是用繼承的方式去撰寫一個汽車的系統,我們可以用剛剛比較的三點來看看:

  1. 封裝性:
    封裝性不用講,繼承的封裝性對子類別來說一定是透明的。

  2. 耦合性:
    電動/汽油車繼承汽車,不同種顏色的車載分別繼承電動/汽油車,耦合性相當的高

  3. 靈活性:
    每當我想要某一個車種,我就必須直接建立該顏色車種的物件,相當的不方便。

現在來使用CARP來解決上述的問題。

CARP2

建立一個interface Color,然後將這個interface合成到Car的抽象類別內。接著再由不同的顏色實作Color,如下:

interface Color {  // 抽出來建立介面
      void kind();
}
abstract class Car {
     Color color;  // 做合成
     abstract void run();
     public Color getColor() {
        return color;
     }
     public void setColor(Color color) {
        this.color = color;
     }
}

class ElectricCar extends Car {
    @Override
    void run() {
        System.out.println("電動車");
    }
}
class PetrolCar extends Car {
    @Override
    void run() {
        System.out.println("汽油車");
    }
}
class White implements Color{
    @Override
    public void kind() {
        System.out.println("白色");
    }
}
class Black implements Color{
    @Override
    public void kind() {
        System.out.println("黑色");
    }
}

public class MyCar {
    public static void main(String args[]) {
        ElectricCar electricCar = new ElectricCar();
        White color = new White();
        electricCar.setColor(color);
        electricCar.getColor().kind();
        electricCar.run();
    }
}

output

白色
電動車

把Color拉出來建立一個介面。再經由不同的顏色去實作,最後類別Car再由合成的方式將Color引入,這是用CARP的方式做更改後的結果。現在依然用剛剛比較的三點來看看:

  1. 封裝性:
    實作Color的White和Black並沒有透露出他們實作的細節,外部只有使用他們的方法而已,保持完整的封裝性。

  2. 耦合性:
    Color和Car沒有了相依,降低了類別間的耦合。

  3. 靈活性:
    Color和Car沒有了相依,只需要選好車款,再把指定的顏色帶入,提升了靈活性。

經過班級與學生、人與器官的例子可以知道,耦合性聚合 < 合成。從車子的例子了解了耦合性合成 < 繼承。

  • 小結


CARP的優缺點
優點
1. 保持封裝性
2. 類別間的耦合度低
3. 程式靈活度高
缺點
1. 建造的系統會有較多的類別需要管理
CARP的目標

盡量使用組合(contains-a)/聚合(has-a)方式來代替繼承(is-a)來達到重複使用的目的

合成以及聚合的差異
合成:部分對象以及整體對象的生命週期是一致的。
聚合:部分對象的生命週期與整體對象沒有關聯,甚至可能更長。
  • 範例程式碼


範例1:CARP-Aggregate
範例2:CARP-Composite
範例3:CARP-inherit
範例4:CARP

  • References


程序员必会的设计模式七大原则之——合成复用原则
合成复用原则——面向对象设计原则
軟體設計原則(七)合成/聚合複用原則(Composite/Aggregate Reuse Principle,CARP)


上一篇
[Day07] 依賴反轉原則 | Dependency Inversion Principle
下一篇
[Day09] 迪米特法則 | Law of Demeter
系列文
從生活中認識Design Pattern30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言